{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src='http://hilpisch.com/taim_logo.png' width=\"350px\" align=\"right\">"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Artificial Intelligence in Finance"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Reinforcement Learning"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "&copy; Dr Yves J Hilpisch | The Python Quants GmbH\n",
    "\n",
    "http://aimachine.io | http://twitter.com/dyjh"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Imports"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import os\n",
    "import math\n",
    "import random\n",
    "import numpy as np\n",
    "import pandas as pd\n",
    "from pylab import plt, mpl\n",
    "plt.style.use('seaborn')\n",
    "mpl.rcParams['savefig.dpi'] = 300\n",
    "mpl.rcParams['font.family'] = 'serif'\n",
    "np.set_printoptions(precision=4, suppress=True)\n",
    "os.environ['PYTHONHASHSEED'] = '0'"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## `CartPole` Environment "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import gym"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env = gym.make('CartPole-v0')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.seed(100)\n",
    "env.action_space.seed(100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.observation_space"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.observation_space.low.astype(np.float16)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.observation_space.high.astype(np.float16)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "state = env.reset()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "state"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.action_space"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.action_space.n"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.action_space.sample()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.action_space.sample() "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = env.action_space.sample()\n",
    "a"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "state, reward, done, info = env.step(a)\n",
    "state, reward, done, info"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.reset()\n",
    "for e in range(1, 200):\n",
    "    a = env.action_space.sample()\n",
    "    state, reward, done, info = env.step(a) \n",
    "    print(f'step={e:2d} | state={state} | action={a} | reward={reward}')\n",
    "    if done and (e + 1) < 200:\n",
    "        print('*** FAILED ***')\n",
    "        break"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "done"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dimensionality Reduction"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "See http://kvfrans.com/simple-algoritms-for-solving-cartpole/."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "np.random.seed(100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "weights = np.random.random(4) * 2 - 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "weights"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "state = env.reset()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "state"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "s = np.dot(state, weights)\n",
    "s"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Action Rule"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "if s < 0:\n",
    "    a = 0\n",
    "else:\n",
    "    a = 1"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Total Reward per Episode"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def run_episode(env, weights):  \n",
    "    state = env.reset()\n",
    "    treward = 0\n",
    "    for _ in range(200):\n",
    "        s = np.dot(state, weights)\n",
    "        a = 0 if s < 0 else 1\n",
    "        state, reward, done, info = env.step(a)\n",
    "        treward += reward\n",
    "        if done:\n",
    "            break\n",
    "    return treward"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "run_episode(env, weights)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Simple Learning "
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def set_seeds(seed=100):\n",
    "    random.seed(seed)\n",
    "    np.random.seed(seed)\n",
    "    env.seed(seed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "set_seeds()\n",
    "num_episodes = 1000"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "besttreward = 0\n",
    "for e in range(1, num_episodes + 1):\n",
    "    weights = np.random.rand(4) * 2 - 1\n",
    "    treward = run_episode(env, weights)\n",
    "    if treward > besttreward:\n",
    "        besttreward = treward\n",
    "        bestweights = weights\n",
    "        if treward == 200:\n",
    "            print(f'SUCCESS | episode={e}')\n",
    "            break\n",
    "        print(f'UPDATE  | episode={e}')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "weights"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Testing the Results"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = []\n",
    "for _ in range(100):\n",
    "    treward = run_episode(env, weights)\n",
    "    res.append(treward)\n",
    "res[:10]"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sum(res) / len(res)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## DNN Learning"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import logging\n",
    "import tensorflow as tf\n",
    "tf.get_logger().setLevel(logging.ERROR)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from tensorflow.python.framework.ops import disable_eager_execution\n",
    "disable_eager_execution()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from keras.layers import Dense, Dropout\n",
    "from keras.models import Sequential\n",
    "from keras.optimizers import Adam, RMSprop\n",
    "from sklearn.metrics import accuracy_score"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def set_seeds(seed=100):\n",
    "    random.seed(seed)\n",
    "    np.random.seed(seed)\n",
    "    tf.random.set_seed(seed)\n",
    "    env.seed(seed)\n",
    "    env.action_space.seed(seed)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class NNAgent:\n",
    "    def __init__(self):\n",
    "        self.max = 0\n",
    "        self.scores = list()\n",
    "        self.memory = list()\n",
    "        self.model = self._build_model()\n",
    "        \n",
    "    def _build_model(self):\n",
    "        model = Sequential()\n",
    "        model.add(Dense(24, input_dim=4,\n",
    "                        activation='relu'))\n",
    "        model.add(Dense(1, activation='sigmoid'))\n",
    "        model.compile(loss='binary_crossentropy',\n",
    "                      optimizer=RMSprop(lr=0.001))\n",
    "        return model\n",
    "        \n",
    "    def act(self, state):\n",
    "        if random.random() <= 0.5:\n",
    "            return env.action_space.sample()\n",
    "        action = np.where(self.model.predict(\n",
    "            state, batch_size=None)[0, 0] > 0.5, 1, 0)\n",
    "        return action\n",
    "                    \n",
    "    def train_model(self, state, action):\n",
    "        self.model.fit(state, np.array([action,]),\n",
    "                       epochs=1, verbose=False)\n",
    "    \n",
    "    def learn(self, episodes):\n",
    "        for e in range(1, episodes + 1):\n",
    "            state = env.reset()\n",
    "            for _ in range(201):\n",
    "                state = np.reshape(state, [1, 4])\n",
    "                action = self.act(state)\n",
    "                next_state, reward, done, info = env.step(action)\n",
    "                if done:\n",
    "                    score = _ + 1\n",
    "                    self.scores.append(score)\n",
    "                    self.max = max(score, self.max)\n",
    "                    print('episode: {:4d}/{} | score: {:3d} | max: {:3d}'\n",
    "                          .format(e, episodes, score, self.max), end='\\r')\n",
    "                    break\n",
    "                self.memory.append((state, action))\n",
    "                self.train_model(state, action)\n",
    "                state = next_state"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "set_seeds(100)\n",
    "agent = NNAgent()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "episodes = 1000"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "agent.learn(episodes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sum(agent.scores) / len(agent.scores)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "f = np.array([m[0][0] for m in agent.memory])\n",
    "f"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "l = np.array([m[1] for m in agent.memory])\n",
    "l"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "accuracy_score(np.where(agent.model.predict(f) > 0.5, 1, 0), l)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Q Learning"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "See https://keon.io/deep-q-learning/"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from collections import deque\n",
    "from keras.optimizers import Adam, RMSprop"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class DQLAgent:\n",
    "    def __init__(self, gamma=0.95, hu=24, opt=Adam,\n",
    "           lr=0.001, finish=False):\n",
    "        self.finish = finish\n",
    "        self.epsilon = 1.0\n",
    "        self.epsilon_min = 0.01\n",
    "        self.epsilon_decay = 0.995\n",
    "        self.gamma = gamma\n",
    "        self.batch_size = 32\n",
    "        self.max_treward = 0\n",
    "        self.averages = list()\n",
    "        self.memory = deque(maxlen=2000)\n",
    "        self.osn = env.observation_space.shape[0]\n",
    "        self.model = self._build_model(hu, opt, lr)\n",
    "        \n",
    "    def _build_model(self, hu, opt, lr):\n",
    "        model = Sequential()\n",
    "        model.add(Dense(hu, input_dim=self.osn,\n",
    "                        activation='relu'))\n",
    "        model.add(Dense(hu, activation='relu'))\n",
    "        model.add(Dense(env.action_space.n, activation='linear'))\n",
    "        model.compile(loss='mse', optimizer=opt(lr=lr))\n",
    "        return model\n",
    "        \n",
    "    def act(self, state):\n",
    "        if random.random() <= self.epsilon:\n",
    "            return env.action_space.sample()\n",
    "        action = self.model.predict(state)[0]\n",
    "        return np.argmax(action)\n",
    "    \n",
    "    def replay(self):\n",
    "        batch = random.sample(self.memory, self.batch_size)\n",
    "        for state, action, reward, next_state, done in batch:\n",
    "            if not done:\n",
    "                reward += self.gamma * np.amax(\n",
    "                    self.model.predict(next_state)[0])\n",
    "            target = self.model.predict(state)\n",
    "            target[0, action] = reward\n",
    "            self.model.fit(state, target, epochs=1,\n",
    "                           verbose=False)\n",
    "        if self.epsilon > self.epsilon_min:\n",
    "            self.epsilon *= self.epsilon_decay\n",
    "    \n",
    "    def learn(self, episodes):\n",
    "        trewards = []\n",
    "        for e in range(1, episodes + 1):\n",
    "            state = env.reset()\n",
    "            state = np.reshape(state, [1, self.osn])\n",
    "            for _ in range(5000):\n",
    "                action = self.act(state)\n",
    "                next_state, reward, done, info = env.step(action)\n",
    "                next_state = np.reshape(next_state,\n",
    "                                        [1, self.osn])\n",
    "                self.memory.append([state, action, reward,\n",
    "                                     next_state, done])\n",
    "                state = next_state\n",
    "                if done:\n",
    "                    treward = _ + 1\n",
    "                    trewards.append(treward)\n",
    "                    av = sum(trewards[-25:]) / 25\n",
    "                    self.averages.append(av)\n",
    "                    self.max_treward = max(self.max_treward, treward)\n",
    "                    templ = 'episode: {:4d}/{} | treward: {:4d} | '\n",
    "                    templ += 'av: {:6.1f} | max: {:4d}'\n",
    "                    print(templ.format(e, episodes, treward, av,\n",
    "                                       self.max_treward), end='\\r')\n",
    "                    break\n",
    "            if av > 195 and self.finish:\n",
    "                print()\n",
    "                break\n",
    "            if len(self.memory) > self.batch_size:\n",
    "                self.replay()\n",
    "    def test(self, episodes):\n",
    "        trewards = []\n",
    "        for e in range(1, episodes + 1):\n",
    "            state = env.reset()\n",
    "            for _ in range(5001):\n",
    "                state = np.reshape(state, [1, self.osn])\n",
    "                action = np.argmax(self.model.predict(state)[0])\n",
    "                next_state, reward, done, info = env.step(action)\n",
    "                state = next_state\n",
    "                if done:\n",
    "                    treward = _ + 1\n",
    "                    trewards.append(treward)\n",
    "                    print('episode: {:4d}/{} | treward: {:4d}'\n",
    "                          .format(e, episodes, treward), end='\\r')\n",
    "                    break\n",
    "        return trewards"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "episodes = 1000"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "set_seeds(100)\n",
    "agent = DQLAgent(finish=True)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%time agent.learn(episodes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10, 6))\n",
    "x = range(len(agent.averages))\n",
    "y = np.polyval(np.polyfit(x, agent.averages, deg=3), x)\n",
    "plt.plot(agent.averages, label='moving average')\n",
    "plt.plot(x, y, 'r--', label='trend')\n",
    "plt.xlabel('episodes')\n",
    "plt.ylabel('total reward')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "trewards = agent.test(100)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "sum(trewards) / len(trewards)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Finance Environment"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class observation_space:\n",
    "    def __init__(self, n):\n",
    "        self.shape = (n,)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class action_space:\n",
    "    def __init__(self, n):\n",
    "        self.n = n\n",
    "    def seed(self, seed):\n",
    "        pass\n",
    "    def sample(self):\n",
    "        return random.randint(0, self.n - 1)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "class Finance:\n",
    "    url = 'http://hilpisch.com/aiif_eikon_eod_data.csv'\n",
    "    def __init__(self, symbol, features):\n",
    "        self.symbol = symbol\n",
    "        self.features = features\n",
    "        self.observation_space = observation_space(4)\n",
    "        self.osn = self.observation_space.shape[0]\n",
    "        self.action_space = action_space(2)\n",
    "        self.min_accuracy = 0.475\n",
    "        self._get_data()\n",
    "        self._prepare_data()\n",
    "    def _get_data(self):\n",
    "        self.raw = pd.read_csv(self.url, index_col=0,\n",
    "                               parse_dates=True).dropna()\n",
    "    def _prepare_data(self):\n",
    "        self.data = pd.DataFrame(self.raw[self.symbol])\n",
    "        self.data['r'] = np.log(self.data / self.data.shift(1))\n",
    "        self.data.dropna(inplace=True)\n",
    "        self.data = (self.data - self.data.mean()) / self.data.std()\n",
    "        self.data['d'] = np.where(self.data['r'] > 0, 1, 0)\n",
    "    def _get_state(self):\n",
    "        return self.data[self.features].iloc[\n",
    "            self.bar - self.osn:self.bar].values\n",
    "    def seed(self, seed=None):\n",
    "        pass\n",
    "    def reset(self):\n",
    "        self.treward = 0\n",
    "        self.accuracy = 0\n",
    "        self.bar = self.osn\n",
    "        state = self.data[self.features].iloc[\n",
    "            self.bar - self.osn:self.bar]\n",
    "        return state.values\n",
    "    def step(self, action):\n",
    "        correct = action == self.data['d'].iloc[self.bar]\n",
    "        reward = 1 if correct else 0\n",
    "        self.treward += reward\n",
    "        self.bar += 1\n",
    "        self.accuracy = self.treward / (self.bar - self.osn)\n",
    "        if self.bar >= len(self.data):\n",
    "            done = True\n",
    "        elif reward == 1:\n",
    "            done = False\n",
    "        elif (self.accuracy < self.min_accuracy and\n",
    "              self.bar > self.osn + 10):\n",
    "            done = True\n",
    "        else:\n",
    "            done = False\n",
    "        state = self._get_state()\n",
    "        info = {}\n",
    "        return state, reward, done, info"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env = Finance('EUR=', 'EUR=')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.reset()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "a = env.action_space.sample()\n",
    "a"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "env.step(a)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "set_seeds(100)\n",
    "agent = DQLAgent(gamma=0.5, opt=RMSprop)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "episodes = 1000"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%time agent.learn(episodes)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "agent.test(3)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "plt.figure(figsize=(10, 6))\n",
    "x = range(len(agent.averages))\n",
    "y = np.polyval(np.polyfit(x, agent.averages, deg=3), x)\n",
    "plt.plot(agent.averages, label='moving average')\n",
    "plt.plot(x, y, 'r--', label='regression')\n",
    "plt.xlabel('episodes')\n",
    "plt.ylabel('total reward')\n",
    "plt.legend();"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "<img src=\"http://hilpisch.com/tpq_logo.png\" alt=\"The Python Quants\" width=\"35%\" align=\"right\" border=\"0\"><br>\n",
    "\n",
    "<a href=\"http://tpq.io\" target=\"_blank\">http://tpq.io</a> | <a href=\"http://twitter.com/dyjh\" target=\"_blank\">@dyjh</a> | <a href=\"mailto:training@tpq.io\">training@tpq.io</a>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.8.6"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 4
}
